#include <QApplication>
#include <QDesktopWidget>
#include <QMessageBox>
#include "debug-helper.h"
#include "icon-store.h"
#include "notification.h"
#include "notifications-mover.h"
#include "notification-displayer.h"
#include "notifier.h"
#include "settings.h"
#include "standard-paths.h"


Notifier::Notifier (QWidget* parent)
    : QObject(parent), parentWidget(parent), removeWidgetLater(false), widgetFadingOut(false), widgetsWaiting(false),
      m_displayMode(NotificationMode), imageSize(28)
{
    backgroundImage = QPixmap(StandardPaths::imagesPath() + "notifications/notification-background.png");
    hintImage = IconStore::pixmap(IconStore::DialogHint, imageSize);
    informationImage = IconStore::pixmap(IconStore::DialogInformation, imageSize);
    warningImage = IconStore::pixmap(IconStore::DialogWarning, imageSize);
    criticalImage = IconStore::pixmap(IconStore::DialogCritical, imageSize);
}


void Notifier::showMessage (MessageType type, const QString &title, const QString &message)
{
    addNotification(type, title, message);
}


void Notifier::showHint (const QString& title, const QString& message)
{
    addNotification(Notifier::Hint, title, message);
}


void Notifier::showInformation (const QString& title, const QString& message)
{
    addNotification(Notifier::Information, title, message);
}


void Notifier::showWarning (const QString& title, const QString& message)
{
    addNotification(Notifier::Warning, title, message);
}


void Notifier::showCritical (const QString& title, const QString& message)
{
    addNotification(Notifier::Critical, title, message);
}


void Notifier::setDisplayMode (DisplayMode displayMode)
{
    m_displayMode = displayMode;
}


Notifier::DisplayMode Notifier::currentDisplayMode() const
{
    return m_displayMode;
}


void Notifier::addNotification (Notifier::MessageType type, const QString& title, const QString& message)
{
    if (m_displayMode == MessageBoxMode)
    {
        addTraditionalNotification(type, title, message);
        return;
    }

    // prepare a notification widget - icon, message and size
    Notification* notification = new Notification(backgroundImage);

    switch (type)
    {
    case Hint:
        notification->setImage(hintImage);
        break;

    case Information:
        notification->setImage(informationImage);
        break;

    case Warning:
        notification->setImage(warningImage);
        break;

    case Critical:
        notification->setImage(criticalImage);
        break;

    default:
        fatalError(type);
    }

    notification->setTitle(title);
    notification->setMessage(message);

    notification->resize(notification->sizeHint());
    if (notification->width() < notification->height())
        notification->resize(notification->height(), notification->width());

    // show notification only if:
    // 1) other notifications are not moving
    // 2) other notifications are not fading out
    // 3) we have enough space available on the screen
    if (notificationsMover == 0 && widgetFadingOut == false)
        if(tryShow(notification))
            return;

    // otherwise, add it to the queue
    queuedNotifications.append(notification);
}


void Notifier::addTraditionalNotification (Notifier::MessageType type, const QString &title, const QString &message)
{
    // TODO: maybe add custom built message boxes with our own icons

    switch (type)
    {
    case Hint:
    case Information:
        QMessageBox::information(parentWidget, title, message);
        break;

    case Warning:
        QMessageBox::warning(parentWidget, title, message);
        break;

    case Critical:
        QMessageBox::critical(parentWidget, title, message);
        break;

    default:
        Q_ASSERT(type == Hint || type == Information || type == Warning || type == Critical);
    }
}


bool Notifier::tryShow (Notification* notification)
{
    QRect availableGeometry = checkAvailableGeometry();

    if (notification->height() < availableGeometry.height())
    {
        showNotification(notification, availableGeometry);
        return true;
    }

    return false;
}


QRect Notifier::checkAvailableGeometry()
{
    // calculate the position of new notification
    QRect availableGeometry;
    QDesktopWidget* desktop = QApplication::desktop();
    if (parentWidget != 0)
        availableGeometry = desktop->availableGeometry(parentWidget);
    else
        availableGeometry = desktop->availableGeometry();

    // if there are more notifications, the new one should be above the last one
    if (notifications.size() != 0)
        availableGeometry.setHeight(notifications.at(notifications.size() - 1)->geometry().y());

    return availableGeometry;
}


void Notifier::showNotification (Notification* notification, const QRect& availableGeometry)
{
    // show notification and place it in a proper location
    notification->show();
    notification->move(availableGeometry.right() + 1 - notification->width(),
                       availableGeometry.bottom() + 1 - notification->height());

    notifications.append(notification);

    // prepare a notification displayer thread
    NotificationDisplayer* notificationDisplayer = new NotificationDisplayer(notification,
                                                                             notificationDisplayers.size());
    connect (notificationDisplayer, &NotificationDisplayer::fadingOutStarted, this, &Notifier::fadingOutStarted);
    connect (this, &Notifier::movingStarted, notificationDisplayer, &NotificationDisplayer::movingStarted);
    connect (this, &Notifier::movingEnded, notificationDisplayer, &NotificationDisplayer::movingEnded);
    connect (this, &Notifier::proceed, notificationDisplayer, &NotificationDisplayer::proceed);
    connect (notificationDisplayer, &NotificationDisplayer::close, this, &Notifier::removeWidget);
    connect (notificationDisplayer, &NotificationDisplayer::finished, this, &Notifier::removeDisplayer);

    notificationDisplayers.append(notificationDisplayer);
    notificationDisplayer->start();
}


void Notifier::checkQueuedNotifications()
{
    if (queuedNotifications.size() != 0)
    {
        while (queuedNotifications.size() > 0)
        {
            Notification* notification = queuedNotifications.first();

            if (tryShow(notification))
                queuedNotifications.removeFirst();
            else
                break;
        }
    }
}


void Notifier::fadingOutStarted (const int& height)
{
    widgetFadingOut = true;

    if (notifications.size() > 1)
    {
        emit movingStarted();
        notificationsMover = new NotificationsMover(height, getNotificationsList());
        // for optimisation reason, only the last moving widget will send a signal that it moved itself
        connect (notifications.at(1), &Notification::widgetMoved, notificationsMover.data(), &NotificationsMover::widgetMoved);
        connect (notificationsMover.data(), &NotificationDisplayer::finished, this, &Notifier::removeMover);
        notificationsMover.data()->start();
    }
}


void Notifier::removeWidget()
{
    if (notificationsMover == 0)
    {
        delete notifications.at(0);
        notifications.removeFirst();
        removeWidgetLater = false;
    }
    else
        removeWidgetLater = true;
}


void Notifier::removeDisplayer()
{
    delete notificationDisplayers.at(0);
    notificationDisplayers.removeFirst();

    widgetFadingOut = false;

    if (notificationsMover == 0)
    {
        checkQueuedNotifications();

        if (widgetsWaiting)
        {
            widgetsWaiting = false;
            emit proceed();
        }
    }
}


void Notifier::removeMover()
{
    delete notificationsMover.data();
    if (removeWidgetLater)
        removeWidget();

    emit movingEnded();

    if (widgetFadingOut == false)
    {
        checkQueuedNotifications();
        emit proceed();
    }
    else
        widgetsWaiting = true;
}


const QList<Notification*>& Notifier::getNotificationsList()
{
    return notifications;
}


Notifier::~Notifier()
{
    if (queuedNotifications.size() != 0)
    {
        foreach (Notification* notification, queuedNotifications)
            delete notification;
    }

    if (notificationsMover != 0)
    {
        connect (this, &Notifier::quitRequest, notificationsMover.data(), &NotificationsMover::quitRequest);
        emit quitRequest();
        notificationsMover.data()->wait();
        delete notificationsMover.data();
    }

    if (notificationDisplayers.size() != 0)
    {
        foreach (NotificationDisplayer* notificationDisplayer, notificationDisplayers)
        {
            connect (this, &Notifier::quitRequest, notificationDisplayer, &NotificationDisplayer::quitRequest);
            emit quitRequest();
            notificationDisplayer->wait();
            delete notificationDisplayer;
        }
    }

    if (notifications.size() != 0)
    {
        foreach (Notification* notification, notifications)
            delete notification;
    }
}
